Skip to content

feat(admin,trpc): company metrics dashboard from PostHog, Stripe, Neon, and Mercury - #6410

Merged
saddlepaddle merged 33 commits into
mainfrom
feat/admin-company-dashboard
Aug 13, 2026
Merged

saddlepaddle merged 33 commits into
mainfrom
feat/admin-company-dashboard

Conversation

@saddlepaddle

@saddlepaddle saddlepaddle commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator

What

The admin app becomes the company metrics dashboard: Company / Product / Growth tabs over a mirror of the PostHog Success Metrics dashboard, plus live business metrics from Stripe, Neon, and Mercury. Stale admin surfaces are deleted.

Company — cash trajectory (full width), net burn, runway, MRR, Enterprise ARR by account, WAU, DAU, burn by vendor
Product — activation funnel, DAU/WAU, activated rate, active orgs, workspace depth, cohort retention
Growth — activation funnel, new site visitors, download CTR, signup→paid, activated rate, MRR, logo retention, churn survival

Where each number comes from

Each system answers only what it is authoritative for:

  • PostHog — the eight event-based insights, referenced by short_id through analytics.getInsightResults (allowlisted registry in packages/trpc). refresh=blocking with no app-side cache: PostHog's own result cache and refresh throttle are the caching layer.
  • Stripe Sigma — MRR, via the Query Run API executing Stripe's own MRR report SQL on demand (12h in-process cache). Matches the dashboard MRR chart exactly.
  • Stripe — enterprise contract values, annualized from active subscriptions.
  • Neon — churn cohorts, logo retention, signup→paid, and the identity half of enterprise accounts (name/logo).
  • Mercury — cash (accounts + Treasury), monthly net burn, runway, and burn by counterparty.

Methodology notes worth reviewing

  • Enterprise ARR is inside MRR, not additive — enterprise bills through Stripe subscriptions, so Thrive's $2k/mo is already in the $14.7k MRR line. The tile says so; do not sum them.
  • Runway divides by net burn (outflows less Stripe payouts). Cash includes raised capital. Verified: since May every non-Stripe inflow is an investor wire, so netting Stripe captures 100% of revenue today. If a customer ever wires directly, Mercury can't distinguish it from an investor wire — the mitigation is billing enterprise through Stripe.
  • Two enterprise deals (Crete, Ramp) are unbilled in Stripe and surface as "unbilled" rows rather than being hidden.
  • Self-serve metrics exclude plan='enterprise' throughout.

Purged

Users page (unbounded listUsers findMany — the row-count freeze), the auth_completed retention card and procedures (that event is no longer emitted), WAU/signups/revenue/traffic charts, leaderboard, demo countdown, range pickers, and the now-unused KV cache layer in posthog-client.

Notable fixes found along the way

  • Date axes were labeled a day early everywhere: analytics dates are calendar strings, but new Date("2026-02-01") parses as UTC midnight and formatted locally renders as January. All formatting now pins to UTC.
  • Retention and churn grids share one CohortGrid component so they can't drift.
  • Registry ships from a constants-only subpath export — exporting it from the tRPC index dragged posthog-node into client bundles.

Deploy

MERCURY_API_TOKEN is set as a production environment secret and passed to the api and admin deploys. Preview deploys intentionally leave it unset (live banking credential); tiles degrade to an explicit "not configured" state.

Plan with the full decision log: plans/done/20260812-1845-admin-company-dashboard.md

https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ

@coderabbitai

coderabbitai Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR replaces the configurable admin analytics dashboard with fixed Product and Business metric sections. It adds PostHog insight retrieval, Stripe and Neon business procedures, reusable metric tiles, and removes obsolete dashboard and user-management surfaces.

Changes

Company dashboard

Layer / File(s) Summary
Insight registry and retrieval
packages/trpc/src/router/analytics/insight-registry.ts, packages/trpc/src/lib/posthog-client.ts, packages/trpc/src/router/analytics/analytics.ts, packages/trpc/src/index.ts
Centralizes eight admin insight identifiers and exposes generic PostHog insight retrieval through tRPC.
Live business metrics
packages/trpc/src/router/analytics/business.ts, packages/trpc/src/root.ts, packages/trpc/src/env.ts
Adds Stripe Sigma and Neon procedures for MRR, churn, logo retention, signup-to-paid, ARPU, and enterprise ARR availability.
Insight and metric tiles
apps/admin/src/app/(dashboard)/components/InsightTileFrame/*, apps/admin/src/app/(dashboard)/components/*Tile/*
Adds shared tile states and charts or tables for Product and Business metrics.
Dashboard composition and cleanup
apps/admin/src/app/(dashboard)/page.tsx, apps/admin/src/app/(dashboard)/components/AppSidebar/*, packages/trpc/src/router/admin/admin.ts, apps/admin/src/app/(dashboard)/users/*, plans/done/*, apps/admin/src/app/(dashboard)/loading.tsx
Renders the fixed Company Metrics dashboard, adds loading placeholders, removes users navigation and procedures, deletes obsolete dashboard components, and records the completed implementation plan.

Estimated code review effort: 4 (Complex) | ~60 minutes

Mergeability Score: 🟠 High · up to 59373

This PR replaces the admin dashboard with live PostHog, Stripe, and Neon metrics and adds polling for Sigma results. At the current head, several unresolved issues can produce incorrect business metrics or cause slow and failed upstream requests to block or repeatedly overload dashboard requests, so the PR should not merge without fixes or explicit owner acceptance.

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant DashboardPage
  participant tRPC
  participant PostHog
  participant StripeSigma
  participant Neon
  Admin->>DashboardPage: open Company Metrics
  DashboardPage->>tRPC: request insight and business metrics
  tRPC->>PostHog: fetch allowlisted insights
  tRPC->>StripeSigma: fetch MRR data
  tRPC->>Neon: query subscription metrics
  PostHog-->>tRPC: return insight results
  StripeSigma-->>tRPC: return MRR points or unavailable state
  Neon-->>tRPC: return business metric rows
  tRPC-->>DashboardPage: return tile data and states
  DashboardPage-->>Admin: render Product and Business tiles
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title uses conventional commit format and clearly summarizes the new admin company metrics dashboard.
Description check ✅ Passed The description thoroughly explains the dashboard scope, data sources, methodology, removals, fixes, deployment, and implementation plan.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/admin-company-dashboard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…admin

Adds an allowlisted registry of the eight event-based insights on the
company metrics dashboard and a thin getInsightResults adminProcedure
(refresh=blocking; PostHog's own result cache is the caching layer).

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…urge stale admin

Admin's dashboard becomes a one-to-one mirror of the PostHog company
metrics dashboard: product tiles reference the eight canonical saved
insights by id (PostHog stays the compute engine and cache); business
tiles compute live — MRR from Stripe's own Sigma MRR report (scheduled
query "admin-mrr", graceful unavailable state until Sigma is enabled),
churn cohorts / logo retention / signup→paid conversion from Neon.

Purged per plan D-11/D-12: users page (unbounded listUsers findMany),
dead auth_completed retention card, WAU/signups/revenue/traffic charts,
leaderboard, demo countdown, range pickers, and their sole-consumer
procedures; posthog-client shrinks to the insight fetcher.

Plan: plans/20260812-1845-admin-company-dashboard.md

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useNewWorkspaceScreenVariant/useNewWorkspaceScreenVariant.ts (1)

35-56: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset variant before each evaluation.

Line 36 leaves the prior assignment in state after the surface closes. If flags do not arrive on a later open, the fallback preserves a prior "test" value because it only replaces null. DashboardNewWorkspaceModal can then redirect with a stale assignment.

Clear variant when the surface closes and before a non-override evaluation starts.

Proposed fix
 useLayoutEffect(() => {
-	if (!isOpen) return;
+	if (!isOpen) {
+		setVariant(null);
+		return;
+	}
 	if (overrideEnabled) {
 		setVariant("test");
 		return;
 	}
+	setVariant(null);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useNewWorkspaceScreenVariant/useNewWorkspaceScreenVariant.ts`
around lines 35 - 56, The useLayoutEffect evaluation flow must reset variant
before re-evaluating and when the modal closes. Update the effect around
evaluate and the isOpen guard to clear variant to null on close and before
starting any non-override flag evaluation, while preserving the overrideEnabled
test assignment and fallback behavior.
🧹 Nitpick comments (5)
apps/admin/src/app/(dashboard)/components/AppSidebar/AppSidebar.tsx (1)

37-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider removing the now-empty sections scaffold.

sections is always empty, so the sections.map(...) block on Lines 78-116 and the Collapsible imports are dead code. The PR removes stale admin surfaces, so deleting the scaffold keeps this file consistent with that goal. If you plan to add sidebar groups again soon, keep it as is.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/admin/src/app/`(dashboard)/components/AppSidebar/AppSidebar.tsx around
lines 37 - 40, Remove the unused empty sections scaffold declared as sections,
along with the sections.map rendering block and its associated Collapsible
imports in AppSidebar. Preserve the remaining sidebar content and behavior.
packages/trpc/src/router/analytics/business.ts (1)

205-231: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the conversion predicate instead of duplicating the EXISTS block.

The same correlated EXISTS subquery appears twice, once for converted and once for conversion_pct. Any edit must be applied in both places. Compute converted in an inner select or a lateral join, then derive the percentage from that column.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/trpc/src/router/analytics/business.ts` around lines 205 - 231,
Refactor the cohort query to evaluate the correlated conversion EXISTS predicate
once, using an inner select or lateral join to expose the converted count/flag.
Make both converted and conversion_pct derive from that shared result, removing
the duplicated EXISTS block while preserving the existing statuses and 30-day
window.
apps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsx (1)

77-77: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Round the survival percentage before display.

Line 72 rounds the value for the background color, but line 77 renders value.surviving_pct unrounded. If the server returns an unrounded ratio, the cell shows a long decimal, for example 33.33333333333333%. The sibling RetentionGridTile rounds the displayed percentage.

♻️ Proposed formatting fix
-												{value ? `${value.surviving_pct}%` : "—"}
+												{value ? `${Math.round(value.surviving_pct)}%` : "—"}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/admin/src/app/`(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsx
at line 77, Update the survival percentage rendering in ChurnHeatmapTile to
round value.surviving_pct before appending the percent sign, matching the
rounding already used for the background color and the sibling RetentionGridTile
display.
apps/admin/src/app/(dashboard)/page.tsx (1)

43-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the expected HogQL column order for each tile.

The xColumn and series[].column values are positional indices into the saved PostHog HogQL result. A maintainer who edits the saved insight SQL and reorders or inserts a SELECT column silently changes the meaning of these indices. The tile then renders a plausible chart with the wrong series, and no error surfaces.

Add a short comment above each HogQLLineTile that records the expected column list, for example -- 0: week, 1: new_creators, 2: activated, 3: activation_pct. This gives the next editor a local reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/admin/src/app/`(dashboard)/page.tsx around lines 43 - 90, Add a concise
comment immediately above each affected HogQLLineTile recording the saved HogQL
result’s expected positional columns, including index 0 used by xColumn and
every series key used by that tile. Ensure the comments accurately match the
current activationRate, activeOrgs, and workspacePercentiles column mappings
without changing chart configuration.
apps/admin/src/app/(dashboard)/components/PostHogFunnelTile/PostHogFunnelTile.tsx (1)

26-34: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Validate both funnel step fields before mapping.

Es6Yu3Lr is documented as a flat six-step array of step objects, so nested breakdown arrays are not expected. If this tile must tolerate malformed PostHog results, require a string name and finite numeric count before mapping. A count-only guard still permits rows with an undefined name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/admin/src/app/`(dashboard)/components/PostHogFunnelTile/PostHogFunnelTile.tsx
around lines 26 - 34, Update the steps validation before the mapping in
PostHogFunnelTile to retain only flat objects with a string name (or
custom_name, if supported) and a finite numeric count. Do not rely solely on
Array.isArray; reject nested breakdown arrays and rows with undefined names
before calculating firstCount and mapping.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/admin/src/app/`(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx:
- Around line 53-70: Update InsightTileFrame to show the error state only when
no tile data is available, so cached children remain visible during failed
background refetches. Add a derived data-presence prop to InsightTileFrame and
pass it from every caller based on that tile’s cached data, while preserving the
existing loading, empty, and children rendering behavior.

In `@packages/trpc/src/router/analytics/business.ts`:
- Around line 118-140: Update the cohort survival query around the subscriptions
CTE and month-offset aggregation so cohort_size is computed once per cohort and
reused as the fixed denominator for every reported offset. Replace the
per-subscription anniversary filter on created_at with a cohort-level cutoff
based on the cohort month boundary, while retaining only offsets whose cohort
period is complete and preserving the existing output fields and ordering.
- Around line 156-184: Extend the months CTE series in the analytics retention
query one month beyond the current newest base month so active contains the
comparison month for the final row. Keep the outer WHERE filter on b.m
unchanged, and preserve the existing self-join and aggregation behavior.
- Around line 40-43: Configure bounded request timeouts for all listed outbound
dashboard calls: in packages/trpc/src/router/analytics/business.ts at lines
40-43, add an AbortSignal.timeout(...) signal to the scheduled-query-runs list
request and also to the result-file request around line 68; in
packages/trpc/src/lib/posthog-client.ts at lines 15-22, add the same timeout
signal to the refresh=blocking insight request. Use the existing fetch options
while ensuring every affected call aborts after a defined timeout.
- Around line 77-94: Replace the manual split logic in the Sigma CSV parsing
flow with the project’s quote-aware CSV parser, ensuring it supports quoted
commas and CRLF line endings. Preserve the existing column validation, point
construction, filtering, and sorting behavior around the parsed header and rows.

---

Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useNewWorkspaceScreenVariant/useNewWorkspaceScreenVariant.ts`:
- Around line 35-56: The useLayoutEffect evaluation flow must reset variant
before re-evaluating and when the modal closes. Update the effect around
evaluate and the isOpen guard to clear variant to null on close and before
starting any non-override flag evaluation, while preserving the overrideEnabled
test assignment and fallback behavior.

---

Nitpick comments:
In `@apps/admin/src/app/`(dashboard)/components/AppSidebar/AppSidebar.tsx:
- Around line 37-40: Remove the unused empty sections scaffold declared as
sections, along with the sections.map rendering block and its associated
Collapsible imports in AppSidebar. Preserve the remaining sidebar content and
behavior.

In
`@apps/admin/src/app/`(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsx:
- Line 77: Update the survival percentage rendering in ChurnHeatmapTile to round
value.surviving_pct before appending the percent sign, matching the rounding
already used for the background color and the sibling RetentionGridTile display.

In
`@apps/admin/src/app/`(dashboard)/components/PostHogFunnelTile/PostHogFunnelTile.tsx:
- Around line 26-34: Update the steps validation before the mapping in
PostHogFunnelTile to retain only flat objects with a string name (or
custom_name, if supported) and a finite numeric count. Do not rely solely on
Array.isArray; reject nested breakdown arrays and rows with undefined names
before calculating firstCount and mapping.

In `@apps/admin/src/app/`(dashboard)/page.tsx:
- Around line 43-90: Add a concise comment immediately above each affected
HogQLLineTile recording the saved HogQL result’s expected positional columns,
including index 0 used by xColumn and every series key used by that tile. Ensure
the comments accurately match the current activationRate, activeOrgs, and
workspacePercentiles column mappings without changing chart configuration.

In `@packages/trpc/src/router/analytics/business.ts`:
- Around line 205-231: Refactor the cohort query to evaluate the correlated
conversion EXISTS predicate once, using an inner select or lateral join to
expose the converted count/flag. Make both converted and conversion_pct derive
from that shared result, removing the duplicated EXISTS block while preserving
the existing statuses and 30-day window.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c863d9d-1aca-4c5a-8033-d6a88cf7fbb9

📥 Commits

Reviewing files that changed from the base of the PR and between f716a19 and a017881.

📒 Files selected for processing (55)
  • apps/admin/src/app/(dashboard)/components/AppSidebar/AppSidebar.tsx
  • apps/admin/src/app/(dashboard)/components/ArpuTile/ArpuTile.tsx
  • apps/admin/src/app/(dashboard)/components/ArpuTile/index.ts
  • apps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsx
  • apps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/index.ts
  • apps/admin/src/app/(dashboard)/components/DemoCountdown/DemoCountdown.tsx
  • apps/admin/src/app/(dashboard)/components/DemoCountdown/index.ts
  • apps/admin/src/app/(dashboard)/components/HogQLLineTile/HogQLLineTile.tsx
  • apps/admin/src/app/(dashboard)/components/HogQLLineTile/index.ts
  • apps/admin/src/app/(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx
  • apps/admin/src/app/(dashboard)/components/InsightTileFrame/index.ts
  • apps/admin/src/app/(dashboard)/components/LeaderboardTable/LeaderboardTable.tsx
  • apps/admin/src/app/(dashboard)/components/LeaderboardTable/index.ts
  • apps/admin/src/app/(dashboard)/components/LogoRetentionTile/LogoRetentionTile.tsx
  • apps/admin/src/app/(dashboard)/components/LogoRetentionTile/index.ts
  • apps/admin/src/app/(dashboard)/components/MrrTile/MrrTile.tsx
  • apps/admin/src/app/(dashboard)/components/MrrTile/index.ts
  • apps/admin/src/app/(dashboard)/components/PostHogFunnelTile/PostHogFunnelTile.tsx
  • apps/admin/src/app/(dashboard)/components/PostHogFunnelTile/index.ts
  • apps/admin/src/app/(dashboard)/components/RetentionCard/RetentionCard.tsx
  • apps/admin/src/app/(dashboard)/components/RetentionCard/index.ts
  • apps/admin/src/app/(dashboard)/components/RetentionGridTile/RetentionGridTile.tsx
  • apps/admin/src/app/(dashboard)/components/RetentionGridTile/index.ts
  • apps/admin/src/app/(dashboard)/components/RevenueTrendChart/RevenueTrendChart.tsx
  • apps/admin/src/app/(dashboard)/components/RevenueTrendChart/index.ts
  • apps/admin/src/app/(dashboard)/components/SignupToPaidTile/SignupToPaidTile.tsx
  • apps/admin/src/app/(dashboard)/components/SignupToPaidTile/index.ts
  • apps/admin/src/app/(dashboard)/components/SignupsTrendChart/SignupsTrendChart.tsx
  • apps/admin/src/app/(dashboard)/components/SignupsTrendChart/index.ts
  • apps/admin/src/app/(dashboard)/components/TimeRangePicker/TimeRangePicker.tsx
  • apps/admin/src/app/(dashboard)/components/TimeRangePicker/index.ts
  • apps/admin/src/app/(dashboard)/components/TrafficSourcesChart/TrafficSourcesChart.tsx
  • apps/admin/src/app/(dashboard)/components/TrafficSourcesChart/index.ts
  • apps/admin/src/app/(dashboard)/components/TrendSeriesTile/TrendSeriesTile.tsx
  • apps/admin/src/app/(dashboard)/components/TrendSeriesTile/index.ts
  • apps/admin/src/app/(dashboard)/components/WAUTrendChart/WAUTrendChart.tsx
  • apps/admin/src/app/(dashboard)/components/WAUTrendChart/index.ts
  • apps/admin/src/app/(dashboard)/components/WeekPicker/WeekPicker.tsx
  • apps/admin/src/app/(dashboard)/components/WeekPicker/index.ts
  • apps/admin/src/app/(dashboard)/page.tsx
  • apps/admin/src/app/(dashboard)/users/components/UsersTable/UsersTable.tsx
  • apps/admin/src/app/(dashboard)/users/components/UsersTable/index.ts
  • apps/admin/src/app/(dashboard)/users/page.tsx
  • apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useNewWorkspaceScreenVariant/useNewWorkspaceScreenVariant.ts
  • packages/shared/src/constants.ts
  • packages/trpc/src/env.ts
  • packages/trpc/src/index.ts
  • packages/trpc/src/lib/posthog-client.ts
  • packages/trpc/src/root.ts
  • packages/trpc/src/router/admin/admin.ts
  • packages/trpc/src/router/analytics/analytics.ts
  • packages/trpc/src/router/analytics/business.ts
  • packages/trpc/src/router/analytics/insight-registry.ts
  • plans/done/20260812-1845-admin-company-dashboard.md
💤 Files with no reviewable changes (21)
  • apps/admin/src/app/(dashboard)/components/LeaderboardTable/index.ts
  • apps/admin/src/app/(dashboard)/components/TrafficSourcesChart/index.ts
  • apps/admin/src/app/(dashboard)/users/components/UsersTable/index.ts
  • apps/admin/src/app/(dashboard)/components/WAUTrendChart/index.ts
  • apps/admin/src/app/(dashboard)/components/SignupsTrendChart/index.ts
  • apps/admin/src/app/(dashboard)/components/DemoCountdown/DemoCountdown.tsx
  • apps/admin/src/app/(dashboard)/components/TimeRangePicker/index.ts
  • apps/admin/src/app/(dashboard)/components/DemoCountdown/index.ts
  • apps/admin/src/app/(dashboard)/components/RetentionCard/index.ts
  • apps/admin/src/app/(dashboard)/users/components/UsersTable/UsersTable.tsx
  • apps/admin/src/app/(dashboard)/users/page.tsx
  • apps/admin/src/app/(dashboard)/components/WeekPicker/index.ts
  • apps/admin/src/app/(dashboard)/components/LeaderboardTable/LeaderboardTable.tsx
  • apps/admin/src/app/(dashboard)/components/TimeRangePicker/TimeRangePicker.tsx
  • apps/admin/src/app/(dashboard)/components/WeekPicker/WeekPicker.tsx
  • apps/admin/src/app/(dashboard)/components/RevenueTrendChart/index.ts
  • apps/admin/src/app/(dashboard)/components/SignupsTrendChart/SignupsTrendChart.tsx
  • apps/admin/src/app/(dashboard)/components/TrafficSourcesChart/TrafficSourcesChart.tsx
  • apps/admin/src/app/(dashboard)/components/RetentionCard/RetentionCard.tsx
  • apps/admin/src/app/(dashboard)/components/WAUTrendChart/WAUTrendChart.tsx
  • apps/admin/src/app/(dashboard)/components/RevenueTrendChart/RevenueTrendChart.tsx

Comment on lines +53 to +70
{isLoading ? (
<div className="space-y-3">
<Skeleton className="h-6 w-full" />
<Skeleton className="h-6 w-4/5" />
<Skeleton className="h-6 w-3/5" />
</div>
) : error ? (
<div className="flex h-[200px] items-center justify-center">
<p className="text-destructive select-text cursor-text text-sm">
{error.message}
</p>
</div>
) : empty ? (
<div className="flex h-[200px] items-center justify-center rounded-md border border-dashed">
<p className="text-muted-foreground text-sm">{emptyLabel}</p>
</div>
) : (
children

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate file map ---'
ast-grep outline 'apps/admin/src/app/(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx' --view expanded

printf '%s\n' '--- frame source ---'
cat -n 'apps/admin/src/app/(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx'

printf '%s\n' '--- frame usages and state props ---'
rg -n -C 4 'InsightTileFrame|isLoading|emptyLabel|error' 'apps/admin/src/app/(dashboard)' -g '*.tsx' -g '*.ts'

Repository: superset-sh/superset

Length of output: 41681


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- React Query dependency and query definitions ---'
rg -n -C 3 '"`@tanstack/react-query`"|`@tanstack/react-query`|useQuery\(' apps/admin package.json bun.lockb bun.lock 2>/dev/null || true

printf '%s\n' '--- InsightTileFrame callers ---'
for f in \
  'apps/admin/src/app/(dashboard)/components/LogoRetentionTile/LogoRetentionTile.tsx' \
  'apps/admin/src/app/(dashboard)/components/TrendSeriesTile/TrendSeriesTile.tsx' \
  'apps/admin/src/app/(dashboard)/components/RetentionGridTile/RetentionGridTile.tsx' \
  'apps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsx' \
  'apps/admin/src/app/(dashboard)/components/HogQLLineTile/HogQLLineTile.tsx' \
  'apps/admin/src/app/(dashboard)/components/SignupToPaidTile/SignupToPaidTile.tsx' \
  'apps/admin/src/app/(dashboard)/components/MrrTile/MrrTile.tsx'; do
  printf '\n--- %s ---\n' "$f"
  cat -n "$f"
done

printf '%s\n' '--- relevant query hooks ---'
rg -n -C 5 'logoRetention|funnelTrends|retention|churn|HogQL|signupToPaid|mrr|useTRPC' 'apps/admin/src' -g '*.ts' -g '*.tsx'

Repository: superset-sh/superset

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path

files = [
    Path("apps/admin/src/app/(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx"),
    Path("apps/admin/src/app/(dashboard)/components/MetricCard/MetricCard.tsx"),
]
for path in files:
    print(f"--- {path} ---")
    for i, line in enumerate(path.read_text().splitlines(), 1):
        print(f"{i:4}: {line}")
PY

printf '%s\n' '--- query result-state handling ---'
rg -n -C 8 'useQuery|queryKey|queryFn|retry|staleTime|refetch|isFetching|isLoading|error' \
  'apps/admin/src/app/(dashboard)' -g '*.ts' -g '*.tsx'

Repository: superset-sh/superset

Length of output: 50376


🌐 Web query:

TanStack Query v5 official docs query states data retained after refetch error isLoading isFetching

💡 Result:

In TanStack Query v5, if a background refetch fails while you have previously successfully fetched data, the query retains that data [1]. In this scenario, the query remains in the 'success' status (isSuccess: true), but it also attaches the error to the state (isError: true, error: ErrorObject) [2][1]. This allows you to decide whether to show the old data, the error, or both [1]. Regarding the loading states during this background refetch error: - isFetching: This will be false once the background refetch completes (even if it ends in an error), as the fetch is no longer in-flight [3][2]. - isLoading: This remains false. In v5, isLoading is a derived state defined as (isPending && isFetching) [4][5]. Since you have existing data, the query is in the 'success' status, not the 'pending' status, so isLoading will not be true [2][4]. In summary, when a background refetch fails: - Data is retained [1]. - status is 'success' [2][1]. - isError is true [1]. - isLoading is false [4][5]. - isFetching is false (after the request finishes) [2][6]. If, instead, you are performing an initial fetch (where no data exists) and it fails, the query enters the 'pending' status, and if you trigger a retry, it will return to a 'pending' and 'fetching' state [1][7][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
# Read-only truth-table check for the frame's current branch ordering.
states = [
    ("initial load", True, None, False),
    ("successful data", False, None, True),
    ("empty result", False, None, False),
    ("background refetch failed with cached data", False, "error", True),
]
for name, is_loading, error, has_data in states:
    if is_loading:
        rendered = "loading"
    elif error:
        rendered = "error"
    elif not has_data:
        rendered = "empty"
    else:
        rendered = "children"
    print(f"{name}: current={rendered}; proposed={('loading' if is_loading and not has_data else 'error' if error and not has_data else 'empty' if not has_data else 'children')}")
PY

printf '%s\n' '--- query client defaults ---'
cat -n apps/admin/src/trpc/query-client.ts

Repository: superset-sh/superset

Length of output: 1115


Keep cached tile content during background failures.

When a background refetch fails, React Query retains cached data and sets error. Gate the error state on the absence of tile data, and pass the derived data-presence state from each InsightTileFrame caller.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/admin/src/app/`(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx
around lines 53 - 70, Update InsightTileFrame to show the error state only when
no tile data is available, so cached children remain visible during failed
background refetches. Add a derived data-presence prop to InsightTileFrame and
pass it from every caller based on that tile’s cached data, while preserving the
existing loading, empty, and children rendering behavior.

Comment on lines +40 to +43
const listResponse = await fetch(
"https://api.stripe.com/v1/sigma/scheduled_query_runs?limit=50",
{ headers: authHeader },
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

New outbound HTTP calls have no request timeout. Every new external call on the admin dashboard path uses bare fetch with no signal, so a stalled upstream holds the tRPC request open without bound. The dashboard fires many of these in parallel on a cold load.

  • packages/trpc/src/router/analytics/business.ts#L40-L43: pass signal: AbortSignal.timeout(...) to the sigma/scheduled_query_runs list request and to the result-file request on Line 68.
  • packages/trpc/src/lib/posthog-client.ts#L15-L22: pass signal: AbortSignal.timeout(...) to the refresh=blocking insight request.
📍 Affects 2 files
  • packages/trpc/src/router/analytics/business.ts#L40-L43 (this comment)
  • packages/trpc/src/lib/posthog-client.ts#L15-L22
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/trpc/src/router/analytics/business.ts` around lines 40 - 43,
Configure bounded request timeouts for all listed outbound dashboard calls: in
packages/trpc/src/router/analytics/business.ts at lines 40-43, add an
AbortSignal.timeout(...) signal to the scheduled-query-runs list request and
also to the result-file request around line 68; in
packages/trpc/src/lib/posthog-client.ts at lines 15-22, add the same timeout
signal to the refresh=blocking insight request. Use the existing fetch options
while ensuring every affected call aborts after a defined timeout.

Comment on lines +77 to +94
const [header, ...rows] = csv.trim().split("\n");
const columns = (header ?? "").split(",");
const monthIndex = columns.indexOf("month_end");
const mrrIndex = columns.indexOf("total_mrr_in_usd");
if (monthIndex === -1 || mrrIndex === -1) {
return { available: false, reason: "unexpected Sigma CSV columns" };
}

const points = rows
.map((row) => {
const cells = row.split(",");
return {
monthEnd: cells[monthIndex] ?? "",
mrrUsd: Number(cells[mrrIndex] ?? Number.NaN),
};
})
.filter((p) => p.monthEnd && Number.isFinite(p.mrrUsd))
.sort((a, b) => a.monthEnd.localeCompare(b.monthEnd));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Stripe Sigma scheduled query run result file CSV format quoting

💡 Result:

Stripe Sigma provides query results in CSV format through scheduled query runs and manual dashboard exports [1][2][3]. While Stripe's official documentation confirms that these exports are provided as CSV files [1][2][4][3], it does not explicitly specify the internal technical implementation details of its CSV quoting or escaping rules (e.g., whether it uses RFC 4180 standard, specific enclosure characters, or how it handles delimiters within fields). For practical purposes, Stripe's CSV exports are generated to be compatible with standard spreadsheet applications and data processing tools [2][3]. If you are programmatically consuming these files, you should rely on standard CSV parsing libraries, which typically handle common quoting conventions automatically. You can access these results via: 1. Email notifications: Stripe sends an email with a link to download the results as a CSV file [1][2]. 2. Webhooks: The sigma.scheduled_query_run.created event provides a file object containing a URL to download the CSV [1][5]. You can retrieve this file using your live secret API key [1]. 3. Dashboard: You can manually trigger or download CSV exports directly from the Sigma interface [2][6][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
file="packages/trpc/src/router/analytics/business.ts"
sed -n '1,150p' "$file"
printf '\nCSV-related dependencies and parsers:\n'
rg -n '"(csv|papaparse|fast-csv|csv-parse|d3-dsv)|csv|parseCsv|parse.*CSV' --glob 'package.json' --glob 'bun.lock' --glob 'packages/**' .
printf '\nRelevant call sites:\n'
rg -n 'month_end|total_mrr_in_usd|available: false|unexpected Sigma CSV columns' packages

Repository: superset-sh/superset

Length of output: 8329


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' 'Package manifests that declare d3 or CSV parsers:'
rg -n -C 2 '"(d3|d3-dsv|csv-parse|papaparse|fast-csv)"' --glob 'package.json' --glob '!node_modules/**' .

printf '\nBehavior probe for the current parsing logic:\n'
python3 - <<'PY'
import csv
import math

def current(text):
    # Equivalent to csv.trim().split("\n"), row.split(","), and the filter.
    text = text.strip()
    parts = text.split("\n")
    header, *rows = parts
    columns = header.split(",") if header is not None else [""]
    month_index = columns.index("month_end") if "month_end" in columns else -1
    mrr_index = columns.index("total_mrr_in_usd") if "total_mrr_in_usd" in columns else -1
    points = []
    if month_index == -1 or mrr_index == -1:
        return ("unexpected Sigma CSV columns", [])
    for row in rows:
        cells = row.split(",")
        month = cells[month_index] if month_index < len(cells) else ""
        raw = cells[mrr_index] if mrr_index < len(cells) else "NaN"
        try:
            mrr = float(raw)
        except ValueError:
            mrr = float("nan")
        if month and math.isfinite(mrr):
            points.append((month, mrr))
    return ("available", points)

cases = {
    "LF simple": "month_end,total_mrr_in_usd\n2025-01-31,100\n",
    "CRLF simple": "month_end,total_mrr_in_usd\r\n2025-01-31,100\r\n",
    "quoted comma before MRR": 'month_end,description,total_mrr_in_usd\n2025-01-31,"plan, annual",100\n',
    "quoted comma in month": '"month_end","total_mrr_in_usd"\n"2025-01-31",100\n',
}
for name, text in cases.items():
    print(name, "=>", current(text))

print("\nStandard CSV parsing of the same cases:")
for name, text in cases.items():
    rows = list(csv.reader(text.splitlines()))
    print(name, "=>", rows)
PY

Repository: superset-sh/superset

Length of output: 208


🏁 Script executed:

#!/bin/bash
set -u
python3 - <<'PY'
import csv
import math

def js_number(value):
    # Model the relevant Number(...) behavior for these CSV values.
    value = value.strip()
    if value == "":
        return 0.0
    try:
        return float(value)
    except ValueError:
        return float("nan")

def current(text):
    parts = text.strip().split("\n")
    header, *rows = parts
    columns = header.split(",")
    month_index = columns.index("month_end") if "month_end" in columns else -1
    mrr_index = columns.index("total_mrr_in_usd") if "total_mrr_in_usd" in columns else -1
    if month_index == -1 or mrr_index == -1:
        return ("unexpected Sigma CSV columns", [])
    points = []
    for row in rows:
        cells = row.split(",")
        month = cells[month_index] if month_index < len(cells) else ""
        mrr = js_number(cells[mrr_index] if mrr_index < len(cells) else "NaN")
        if month and math.isfinite(mrr):
            points.append((month, mrr))
    return ("available", points)

cases = {
    "LF simple": "month_end,total_mrr_in_usd\n2025-01-31,100\n",
    "CRLF simple": "month_end,total_mrr_in_usd\r\n2025-01-31,100\r\n",
    "quoted comma before MRR": 'month_end,description,total_mrr_in_usd\n2025-01-31,"plan, annual",100\n',
    "quoted fields": '"month_end","total_mrr_in_usd"\n"2025-01-31","100"\n',
    "quoted comma in MRR": 'month_end,total_mrr_in_usd\n2025-01-31,"1,000"\n',
}

for name, text in cases.items():
    print(name, "current =>", current(text))
    print(name, "CSV reader =>", list(csv.reader(text.splitlines())))
PY

Repository: superset-sh/superset

Length of output: 920


🌐 Web query:

site:docs.stripe.com Sigma CSV quoted fields CRLF export format

💡 Result:

Sigma Computing—specifically the Sigma component within the Stripe ecosystem—exports query results in CSV format [1][2]. While official documentation does not explicitly detail the specific RFC compliance (such as quoting behavior or line-ending characters like CRLF) for these generated files, standard CSV exports from such platforms typically follow industry-standard practices, which include wrapping fields containing delimiters or line breaks in quotes and using CRLF (Carriage Return + Line Feed) as the row delimiter [1][2]. Users can trigger these exports via the Stripe Dashboard or programmatically through the Sigma Query Run API [1][3][4].

Citations:


Parse the Sigma result with a quote-aware CSV parser.

The current parser treats CRLF headers as invalid and drops valid rows when quoted fields contain commas. Use a parser that handles quoted fields and CRLF line endings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/trpc/src/router/analytics/business.ts` around lines 77 - 94, Replace
the manual split logic in the Sigma CSV parsing flow with the project’s
quote-aware CSV parser, ensuring it supports quoted commas and CRLF line
endings. Preserve the existing column validation, point construction, filtering,
and sorting behavior around the parsed header and rows.

Comment on lines +118 to +140
}>(sql`
WITH subs AS (
SELECT created_at, ended_at, date_trunc('month', created_at) AS cohort
FROM subscriptions
WHERE status != 'incomplete' AND plan != 'enterprise'
AND created_at >= date_trunc('month', now()) - make_interval(months => ${input.months})
)
SELECT
to_char(cohort, 'YYYY-MM') AS cohort_month,
k.k AS month_offset,
count(*)::int AS cohort_size,
round(
100.0 * count(*) FILTER (
WHERE ended_at IS NULL OR ended_at >= created_at + make_interval(months => k.k)
) / count(*),
1
)::float AS surviving_pct
FROM subs
CROSS JOIN generate_series(0, ${input.months}) AS k(k)
WHERE created_at + make_interval(months => k.k) <= now()
GROUP BY cohort_month, k.k
ORDER BY cohort_month, k.k
`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Cohort survival uses a shrinking denominator.

WHERE created_at + make_interval(months => k.k) <= now() filters individual subscription rows, not whole cohorts. For each larger k, subscriptions created later inside the cohort month drop out of the group. So cohort_size differs per month_offset, and surviving_pct divides by that reduced count. Survival reads high because short-lived recent subscriptions leave the denominator.

A cohort heatmap needs one fixed denominator per cohort and a per-cohort cutoff for how many offsets are complete. Compute cohort_size once per cohort, then gate offsets on the cohort month boundary instead of each subscription's own anniversary.

The static analysis SQL-injection findings on this query and the other three are false positives. input.months and input.weeks are zod-validated numbers, and drizzle binds template values as parameters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/trpc/src/router/analytics/business.ts` around lines 118 - 140,
Update the cohort survival query around the subscriptions CTE and month-offset
aggregation so cohort_size is computed once per cohort and reused as the fixed
denominator for every reported offset. Replace the per-subscription anniversary
filter on created_at with a cohort-level cutoff based on the cohort month
boundary, while retaining only offsets whose cohort period is complete and
preserving the existing output fields and ordering.

Source: Linters/SAST tools

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/trpc/src/router/analytics/business.ts`:
- Around line 85-93: Update the row mapping that builds points so the MRR cell
is trimmed and blank values become Number.NaN before numeric conversion.
Preserve numeric conversion for non-empty values, allowing the existing
Number.isFinite filter to exclude empty MRR cells while retaining valid zero
values.

In `@plans/done/20260812-1845-admin-company-dashboard.md`:
- Around line 116-120: Update the completed plan’s procedure references to match
the shipped business router, replacing business.getRevenueTrend,
business.getNrr, and business.getActivatedToPaid with the exported procedures
actually used by the dashboard. Correct the signup-to-paid input description by
removing user_signed_up and stating that the calculation uses users.created_at,
including the affected sections and repeated references.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a078828d-6cdf-4413-a1ad-97644859ad4c

📥 Commits

Reviewing files that changed from the base of the PR and between f716a19 and 0d2a0d1.

📒 Files selected for processing (52)
  • apps/admin/src/app/(dashboard)/components/AppSidebar/AppSidebar.tsx
  • apps/admin/src/app/(dashboard)/components/ArpuTile/ArpuTile.tsx
  • apps/admin/src/app/(dashboard)/components/ArpuTile/index.ts
  • apps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsx
  • apps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/index.ts
  • apps/admin/src/app/(dashboard)/components/DemoCountdown/DemoCountdown.tsx
  • apps/admin/src/app/(dashboard)/components/DemoCountdown/index.ts
  • apps/admin/src/app/(dashboard)/components/HogQLLineTile/HogQLLineTile.tsx
  • apps/admin/src/app/(dashboard)/components/HogQLLineTile/index.ts
  • apps/admin/src/app/(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx
  • apps/admin/src/app/(dashboard)/components/InsightTileFrame/index.ts
  • apps/admin/src/app/(dashboard)/components/LeaderboardTable/LeaderboardTable.tsx
  • apps/admin/src/app/(dashboard)/components/LeaderboardTable/index.ts
  • apps/admin/src/app/(dashboard)/components/LogoRetentionTile/LogoRetentionTile.tsx
  • apps/admin/src/app/(dashboard)/components/LogoRetentionTile/index.ts
  • apps/admin/src/app/(dashboard)/components/MrrTile/MrrTile.tsx
  • apps/admin/src/app/(dashboard)/components/MrrTile/index.ts
  • apps/admin/src/app/(dashboard)/components/PostHogFunnelTile/PostHogFunnelTile.tsx
  • apps/admin/src/app/(dashboard)/components/PostHogFunnelTile/index.ts
  • apps/admin/src/app/(dashboard)/components/RetentionCard/RetentionCard.tsx
  • apps/admin/src/app/(dashboard)/components/RetentionCard/index.ts
  • apps/admin/src/app/(dashboard)/components/RetentionGridTile/RetentionGridTile.tsx
  • apps/admin/src/app/(dashboard)/components/RetentionGridTile/index.ts
  • apps/admin/src/app/(dashboard)/components/RevenueTrendChart/RevenueTrendChart.tsx
  • apps/admin/src/app/(dashboard)/components/RevenueTrendChart/index.ts
  • apps/admin/src/app/(dashboard)/components/SignupToPaidTile/SignupToPaidTile.tsx
  • apps/admin/src/app/(dashboard)/components/SignupToPaidTile/index.ts
  • apps/admin/src/app/(dashboard)/components/SignupsTrendChart/SignupsTrendChart.tsx
  • apps/admin/src/app/(dashboard)/components/SignupsTrendChart/index.ts
  • apps/admin/src/app/(dashboard)/components/TimeRangePicker/TimeRangePicker.tsx
  • apps/admin/src/app/(dashboard)/components/TimeRangePicker/index.ts
  • apps/admin/src/app/(dashboard)/components/TrafficSourcesChart/TrafficSourcesChart.tsx
  • apps/admin/src/app/(dashboard)/components/TrafficSourcesChart/index.ts
  • apps/admin/src/app/(dashboard)/components/TrendSeriesTile/TrendSeriesTile.tsx
  • apps/admin/src/app/(dashboard)/components/TrendSeriesTile/index.ts
  • apps/admin/src/app/(dashboard)/components/WAUTrendChart/WAUTrendChart.tsx
  • apps/admin/src/app/(dashboard)/components/WAUTrendChart/index.ts
  • apps/admin/src/app/(dashboard)/components/WeekPicker/WeekPicker.tsx
  • apps/admin/src/app/(dashboard)/components/WeekPicker/index.ts
  • apps/admin/src/app/(dashboard)/page.tsx
  • apps/admin/src/app/(dashboard)/users/components/UsersTable/UsersTable.tsx
  • apps/admin/src/app/(dashboard)/users/components/UsersTable/index.ts
  • apps/admin/src/app/(dashboard)/users/page.tsx
  • packages/trpc/src/env.ts
  • packages/trpc/src/index.ts
  • packages/trpc/src/lib/posthog-client.ts
  • packages/trpc/src/root.ts
  • packages/trpc/src/router/admin/admin.ts
  • packages/trpc/src/router/analytics/analytics.ts
  • packages/trpc/src/router/analytics/business.ts
  • packages/trpc/src/router/analytics/insight-registry.ts
  • plans/done/20260812-1845-admin-company-dashboard.md
💤 Files with no reviewable changes (21)
  • apps/admin/src/app/(dashboard)/users/page.tsx
  • apps/admin/src/app/(dashboard)/components/WAUTrendChart/index.ts
  • apps/admin/src/app/(dashboard)/components/SignupsTrendChart/SignupsTrendChart.tsx
  • apps/admin/src/app/(dashboard)/components/WeekPicker/index.ts
  • apps/admin/src/app/(dashboard)/components/TrafficSourcesChart/index.ts
  • apps/admin/src/app/(dashboard)/components/TrafficSourcesChart/TrafficSourcesChart.tsx
  • apps/admin/src/app/(dashboard)/users/components/UsersTable/UsersTable.tsx
  • apps/admin/src/app/(dashboard)/components/RetentionCard/index.ts
  • apps/admin/src/app/(dashboard)/components/LeaderboardTable/index.ts
  • apps/admin/src/app/(dashboard)/components/DemoCountdown/index.ts
  • apps/admin/src/app/(dashboard)/components/RevenueTrendChart/RevenueTrendChart.tsx
  • apps/admin/src/app/(dashboard)/components/DemoCountdown/DemoCountdown.tsx
  • apps/admin/src/app/(dashboard)/components/WeekPicker/WeekPicker.tsx
  • apps/admin/src/app/(dashboard)/components/RetentionCard/RetentionCard.tsx
  • apps/admin/src/app/(dashboard)/components/LeaderboardTable/LeaderboardTable.tsx
  • apps/admin/src/app/(dashboard)/users/components/UsersTable/index.ts
  • apps/admin/src/app/(dashboard)/components/RevenueTrendChart/index.ts
  • apps/admin/src/app/(dashboard)/components/TimeRangePicker/index.ts
  • apps/admin/src/app/(dashboard)/components/TimeRangePicker/TimeRangePicker.tsx
  • apps/admin/src/app/(dashboard)/components/SignupsTrendChart/index.ts
  • apps/admin/src/app/(dashboard)/components/WAUTrendChart/WAUTrendChart.tsx
🚧 Files skipped from review as they are similar to previous changes (28)
  • apps/admin/src/app/(dashboard)/components/LogoRetentionTile/index.ts
  • apps/admin/src/app/(dashboard)/components/HogQLLineTile/index.ts
  • apps/admin/src/app/(dashboard)/components/AppSidebar/AppSidebar.tsx
  • apps/admin/src/app/(dashboard)/components/ArpuTile/index.ts
  • packages/trpc/src/root.ts
  • apps/admin/src/app/(dashboard)/components/MrrTile/index.ts
  • apps/admin/src/app/(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx
  • apps/admin/src/app/(dashboard)/components/HogQLLineTile/HogQLLineTile.tsx
  • apps/admin/src/app/(dashboard)/components/RetentionGridTile/index.ts
  • packages/trpc/src/index.ts
  • packages/trpc/src/env.ts
  • packages/trpc/src/router/analytics/insight-registry.ts
  • apps/admin/src/app/(dashboard)/components/RetentionGridTile/RetentionGridTile.tsx
  • apps/admin/src/app/(dashboard)/components/TrendSeriesTile/index.ts
  • apps/admin/src/app/(dashboard)/components/PostHogFunnelTile/PostHogFunnelTile.tsx
  • apps/admin/src/app/(dashboard)/components/SignupToPaidTile/index.ts
  • apps/admin/src/app/(dashboard)/page.tsx
  • apps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsx
  • apps/admin/src/app/(dashboard)/components/ArpuTile/ArpuTile.tsx
  • apps/admin/src/app/(dashboard)/components/PostHogFunnelTile/index.ts
  • packages/trpc/src/router/admin/admin.ts
  • packages/trpc/src/lib/posthog-client.ts
  • apps/admin/src/app/(dashboard)/components/LogoRetentionTile/LogoRetentionTile.tsx
  • packages/trpc/src/router/analytics/analytics.ts
  • apps/admin/src/app/(dashboard)/components/InsightTileFrame/index.ts
  • apps/admin/src/app/(dashboard)/components/TrendSeriesTile/TrendSeriesTile.tsx
  • apps/admin/src/app/(dashboard)/components/MrrTile/MrrTile.tsx
  • apps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/index.ts

Comment on lines +85 to +93
const points = rows
.map((row) => {
const cells = row.split(",");
return {
monthEnd: cells[monthIndex] ?? "",
mrrUsd: Number(cells[mrrIndex] ?? Number.NaN),
};
})
.filter((p) => p.monthEnd && Number.isFinite(p.mrrUsd))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reject empty MRR cells before numeric conversion.

Number("") returns 0. A blank total_mrr_in_usd value therefore passes Number.isFinite and creates a false zero-MRR point. Trim the cell and treat an empty value as Number.NaN before filtering.

Proposed fix
 		.map((row) => {
 			const cells = row.split(",");
+			const rawMrr = cells[mrrIndex]?.trim();
 			return {
 				monthEnd: cells[monthIndex] ?? "",
-				mrrUsd: Number(cells[mrrIndex] ?? Number.NaN),
+				mrrUsd: rawMrr ? Number(rawMrr) : Number.NaN,
 			};
 		})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const points = rows
.map((row) => {
const cells = row.split(",");
return {
monthEnd: cells[monthIndex] ?? "",
mrrUsd: Number(cells[mrrIndex] ?? Number.NaN),
};
})
.filter((p) => p.monthEnd && Number.isFinite(p.mrrUsd))
const points = rows
.map((row) => {
const cells = row.split(",");
const rawMrr = cells[mrrIndex]?.trim();
return {
monthEnd: cells[monthIndex] ?? "",
mrrUsd: rawMrr ? Number(rawMrr) : Number.NaN,
};
})
.filter((p) => p.monthEnd && Number.isFinite(p.mrrUsd))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/trpc/src/router/analytics/business.ts` around lines 85 - 93, Update
the row mapping that builds points so the MRR cell is trimmed and blank values
become Number.NaN before numeric conversion. Preserve numeric conversion for
non-empty values, allowing the existing Number.isFinite filter to exclude empty
MRR cells while retaining valid zero values.

Comment on lines +116 to +120
Re-implemented live in admin against Neon/Stripe (replacing stale-warehouse tiles and placeholders):

- `zjqYXl1N` "Recurring revenue — monthly recognized" and `uJv3GdAE` "NRR — monthly" → live Stripe-backed procedures (the PostHog versions read the stale Stripe mirror).
- `DhKKxIJx` "Paid churn — cohort survival heatmap" → live Neon `subscriptions` query.
- The three placeholders `veHqMB5A` (activated→paid), `wQpHYjlf` (ARPU), `TO8d4iH9` (enterprise ARR) → real implementations, which is the whole reason they were blocked in PostHog.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the completed plan with the shipped business router.

The plan names business.getRevenueTrend, business.getNrr, and business.getActivatedToPaid, but the supplied router does not export them. It also describes user_signed_up as a signup-to-paid input after D-4 records that the event was removed. Replace these sections with the shipped procedures and their actual inputs. State that signup-to-paid uses users.created_at.

Also applies to: 134-134, 157-159

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plans/done/20260812-1845-admin-company-dashboard.md` around lines 116 - 120,
Update the completed plan’s procedure references to match the shipped business
router, replacing business.getRevenueTrend, business.getNrr, and
business.getActivatedToPaid with the exported procedures actually used by the
dashboard. Correct the signup-to-paid input description by removing
user_signed_up and stating that the calculation uses users.created_at, including
the affected sections and repeated references.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

…s in business metrics

Found by dry-running against a prod-data Neon branch: users/members live
in the auth schema in production (raw SQL bypasses Drizzle's pgSchema
mapping), the logo-retention months series stopped one month short so
the newest base month read 0% retained, and signup→paid included a
partial trailing week.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Replaces the dashboard-managed scheduled Sigma query: getMrr now POSTs
Stripe's own MRR template to /v2/data/reporting/query_runs, polls to
completion, and parses the CSV — 12h in-process cache, concurrent
callers share one run. Verified live: matches the dashboard MRR chart
(July 2026: $14,510).

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
… unbilled deals

Replaces the not-yet-tracked placeholder: ARR is the annualized amounts
of active Stripe subscriptions for plan=enterprise orgs (Neon names
which orgs are enterprise; Stripe supplies the money). Deals without a
priced Stripe subscription are counted as unbilled logos on the tile
instead of silently omitted — the bookkeeping contract is that every
enterprise contract exists as a priced Stripe subscription with wires
marked paid out-of-band on the subscription invoice.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Removes the leftover stacking wrapper from the ARPA removal; the card
now takes the row's full height via h-full (grid items stretch).

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…x axes

DAU and WAU are now separate PostHog insights (7mbktvP7 / 82RAL00l) and
separate admin tiles. Shared chartAxis util applies the axis rule
everywhere: ranges beyond ~2 months tick once per month labeled with the
month name, shorter ranges label dates as "Aug 8"; non-date axes
(percentiles) pass through unchanged.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Replaces the horizontal Recharts bars with PostHog's funnel layout: one
column per step, solid fill as % of step one over a hatched drop-off
region, with converted/dropped persons, step-over-step %, and median
conversion time under each column. The OR-group step gets its real name
instead of the '$pageview, $pageview, …' mush.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Funnel columns get two hover targets — solid region shows converted /
conversion-from-previous / conversion-so-far / median+average time,
hatched region shows dropped / drop-off-from-previous / drop-off-from-
start — with full-contrast tooltip values. Retention grid redesigned as
rounded chips with a size-weighted Mean row, Week N headers, decimal
percentages, dashed outline for the in-progress diagonal cell, and no
cells for future weeks (previously misleading 0%s).

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…tip spacing

WAU insight switched from rolling-7d to calendar-week intervals (one
entry per week, PostHog pattern from the Superset Dashboard's WAU
chart). Weekly tiles (WAU, p50/p90, download CTR) draw the in-progress
week as a dashed final segment. Shared chart tooltip gains a gap
between label and value so long labels don't collide with numbers.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
bg-background container with border, muted-foreground labels, and
foreground mono values — identical contrast to the shared chart
tooltips instead of the UI tooltip primitive's primary tokens.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Tiles are organized by who cares, and can appear on several tabs:
Company shows DAU/WAU/MRR/Enterprise ARR; Product shows the activation
funnel, DAU/WAU, activated rate, active orgs, workspace depth, and
cohort retention; Growth shows the funnel, site visitors, download CTR,
signup→paid, activated rate, MRR, logo retention, and churn cohorts.
Enterprise ARR extracted into its own tile component.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…mpany tab

getCashFlow reads Mercury accounts + Treasury (where the raise is
parked) for total cash, and aggregates monthly transactions across all
accounts with treasury sweeps excluded. Runway = cash / avg gross ops
outflows over the last 3 complete months — net flow includes fundraise
tranche wires, so it decorates the cash card instead of driving runway
or the chart. Company tab gains a Cash & runway card and a monthly
gross-burn chart.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…y, burn by vendor

Headline stats (cash / gross burn / runway) lead the tab, followed by
the total-cash balance over time (tranche wires as steps), monthly gross
burn, and average monthly outflow per counterparty over the last 3
complete months — the actionable view of where money goes. Cash series
is reconstructed by walking transactions back from today's balances.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
… fix

Net burn nets Stripe payouts against outflows (runway now 211 mo at
$53k/mo net vs $69.5k gross); the burn chart plots net burn beside
Stripe revenue. Enterprise ARR becomes a per-account list with org name,
logo, and contracted ARR from Stripe — unbilled deals stay visible.
Retention Mean row now includes weeks only reached by in-progress
cohorts, rendering dashed like the cohort cells instead of blank.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…usive

Enterprise subscriptions bill through Stripe, so their ARR is already
counted in the Sigma MRR line — the tile now says so to stop anyone
adding the two. Cash card notes it includes raised capital.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
The only sub-zero value is the partial current month, which was pulling
the axis to -$50k and wasting half the chart.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…rn months

Analytics dates are calendar strings, but new Date('2026-02-01') parses
as UTC midnight and formatted in local time — every date axis was
labeled one day early, rendering February as January. All formatting now
pins to UTC in the shared chartAxis util.

Retention and churn-survival grids now share one CohortGrid component so
they can't drift apart: chips scaled by percentage, dashed while a
period is in progress, blank for periods not yet reached. Churn gains
the retention grid's styling (it was still the old smushed pills).

The burn chart drops months with no outflows — pre-operating noise that
produced negative burn and dragged the axis to -$50k.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Drops the Cash and Net burn stat cards — both duplicated their charts.
Cash trajectory goes full width, with the net burn chart and a runway
card (carrying the cash ÷ burn inputs in its subtitle) side by side
underneath.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Stat cards stretched to match chart-height siblings left their value
pinned to the top-left of a mostly empty card.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Cash, burn, and runway on the admin dashboard read Mercury. Production
only — preview deploys leave it unset (it is a live banking credential
and preview URLs are more exposed), where the tiles degrade to an
explicit "not configured" state.

Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ

This branch was previously deployed

1 inactive deployment
preview — 6a30a8b8 Deployed Aug 13, 2026 by saddlepaddle via Deploy Docs #7675
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant